package system

import (
	"MoSkeleton/config/consts"
	"MoSkeleton/framework"
	consts2 "MoSkeleton/framework/core/consts"
	"MoSkeleton/framework/core/dto/request"
	"MoSkeleton/framework/core/models"
	"MoSkeleton/framework/core/moerrors"
	"MoSkeleton/framework/utils"
	"MoSkeleton/models/system"
	"MoSkeleton/services/base"
	"MoSkeleton/web/pogos"
	s1 "MoSkeleton/web/pogos/request/system"
	system2 "MoSkeleton/web/pogos/response/system"
	"errors"
	mapset "github.com/deckarep/golang-set/v2"
	"github.com/samber/lo"
	"gorm.io/gorm"
	"strings"
)

type SysMenuService struct {
	base.BaseService
}

/*
select distinct m.perms

	from sys_menu m
		 left join sys_role_menu rm on m.menu_id = rm.menu_id
		 left join sys_user_role ur on rm.role_id = ur.role_id
		 left join sys_role r on r.role_id = ur.role_id
	where m.status = '0' and r.status = '0' and ur.user_id = #{userId}
*/
func (s *SysMenuService) GetMenuPermsByUserId(userId uint64) (*mapset.Set[string], error) {
	sysUser := system.SysUser{
		MoModel: models.MoModel{
			ID: userId,
		},
	}
	if sysUser.IsAdmin() {
		set := mapset.NewSet[string](consts.Permission_All)
		return &set, nil
	}
	var menus []system.SysMenu
	if err := base.BaseDb.Table("sys_menu m").Distinct("m.perms").Joins("left join sys_role_menu rm on m.id = rm.menu_id").
		Joins("left join sys_user_role ur on rm.role_id = ur.role_id").Joins("left join sys_role r on r.id = ur.role_id").
		Where("m.status = ? and r.status = ? and ur.user_id = ?", "0", "0", userId).Find(&menus).Error; err != nil {
		return nil, err
	}
	if len(menus) == 0 {
		set := mapset.NewSet[string]()
		return &set, nil
	}
	menuPerms := mapset.NewSet[string]()
	for _, menu := range menus {
		menuPerms.Add(menu.Perms)
	}
	return &menuPerms, nil
}

func (s *SysMenuService) BuildRouterByMenuTree(menuTree []*system2.SysMenuTreeResp) []*system2.RouterResp {
	var routers []*system2.RouterResp
	for _, treeItem := range menuTree {
		router := &system2.RouterResp{
			Hidden:    "1" == treeItem.Visible,
			Name:      getRouteName(&treeItem.SysMenu),
			Path:      getRouterPath(&treeItem.SysMenu),
			Component: getComponent(&treeItem.SysMenu),
			Query:     treeItem.Query,
			Meta: &system2.MetaResp{
				Title:   treeItem.MenuName,
				Icon:    treeItem.Icon,
				NoCache: 1 == treeItem.SysMenu.IsCache,
			},
		}
		router.Meta.SetLink(treeItem.Path)

		if len(treeItem.Children) > 0 && consts.U_TYPE_DIR == treeItem.MenuType {
			router.AlwaysShow = true
			router.Redirect = "noRedirect"
			router.Children = s.BuildRouterByMenuTree(treeItem.Children)
		} else if isMenuFrame(&treeItem.SysMenu) {
			router.Meta = nil
			childrenList := make([]*system2.RouterResp, 0)
			children := &system2.RouterResp{
				Path:      treeItem.Path,
				Component: treeItem.Component,
				Name:      getRouteNameOrPath(treeItem.RouteName, treeItem.Path),
				Meta: &system2.MetaResp{
					Title:   treeItem.MenuName,
					Icon:    treeItem.Icon,
					NoCache: 1 == treeItem.SysMenu.IsCache,
				},
				Query: treeItem.Query,
			}
			children.Meta.SetLink(treeItem.Path)
			childrenList = append(childrenList, children)
			router.Children = childrenList
		} else if treeItem.ParentId == 0 && isInnerLink(&treeItem.SysMenu) {
			router.Meta = &system2.MetaResp{
				Title: treeItem.MenuName,
				Icon:  treeItem.Icon,
			}
			router.Path = "/"
			childrenList := make([]*system2.RouterResp, 0)
			children := &system2.RouterResp{
				Path:      innerLinkReplaceEach(treeItem.Path),
				Component: consts.U_INNER_LINK,
				Name:      getRouteNameOrPath(treeItem.RouteName, treeItem.Path),
				Meta: &system2.MetaResp{
					Title: treeItem.MenuName,
					Icon:  treeItem.Icon,
				},
			}
			children.Meta.SetLink(treeItem.Path)
			childrenList = append(childrenList, children)
			router.Children = childrenList
		}
		routers = append(routers, router)
	}
	return routers
}

func (s *SysMenuService) GetMenuTreeByUserId(userId uint64) ([]*system2.SysMenuTreeResp, error) {
	sysUser := system.SysUser{
		MoModel: models.MoModel{
			ID: userId,
		},
	}
	var menus []*system.SysMenu
	var err error
	if sysUser.IsAdmin() {
		menus, err = s.GetAllMenuForTree()
		if err != nil {
			return nil, err
		}
	} else {
		menus, err = s.GetMenuForTreeByUserId(userId)
		if err != nil {
			return nil, err
		}
	}
	var menuTree []*system2.SysMenuTreeResp
	if menuTree, err = s.genMenuTreeByList(menus, 0); err != nil {
		return nil, err
	}
	return menuTree, nil
}

func (s *SysMenuService) GetAllMenuByUserId(listReq *s1.SysMenuListReq, userId uint64) ([]*system.SysMenu, error) {
	sysUser := system.SysUser{
		MoModel: models.MoModel{
			ID: userId,
		},
	}
	if sysUser.IsAdmin() {
		return s.getAllMenuForList(listReq)
	} else {
		return s.getMenuListByUserId(listReq, userId)
	}
}

/*
	 select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time
			from sys_menu

<where>

		<if test="menuName != null and menuName != ''">
			AND menu_name like concat('%', #{menuName}, '%')
		</if>
		<if test="visible != null and visible != ''">
			AND visible = #{visible}
		</if>
		<if test="status != null and status != ''">
			AND status = #{status}
		</if>
	</where>
	order by parent_id, order_num
*/
func (s *SysMenuService) getAllMenuForList(listReq *s1.SysMenuListReq) ([]*system.SysMenu, error) {
	selectFields := "id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, created_at"
	db := base.BaseDb.Model(&system.SysMenu{}).Select(selectFields)
	if listReq.MenuName != "" {
		db.Where("menu_name like ?", "%"+listReq.MenuName+"%")
	}
	if listReq.Visible != "" {
		db.Where("visible = ?", listReq.Visible)
	}
	if listReq.Status != "" {
		db.Where("status = ?", listReq.Status)
	}
	result := make([]*system.SysMenu, 0)
	err := db.Order("parent_id, order_num").Find(&result).Error
	return result, err
}

/*
select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,”) as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time

			from sys_menu m
			left join sys_role_menu rm on m.menu_id = rm.menu_id
			left join sys_user_role ur on rm.role_id = ur.role_id
			left join sys_role ro on ur.role_id = ro.role_id
			where ur.user_id = #{params.userId}
			<if test="menuName != null and menuName != ''">
	            AND m.menu_name like concat('%', #{menuName}, '%')
			</if>
			<if test="visible != null and visible != ''">
	            AND m.visible = #{visible}
			</if>
			<if test="status != null and status != ''">
	            AND m.status = #{status}
			</if>
			order by m.parent_id, m.order_num
*/
func (s *SysMenuService) getMenuListByUserId(listReq *s1.SysMenuListReq, userId uint64) ([]*system.SysMenu, error) {
	selectFields := "m.id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.created_at"
	db := base.BaseDb.Table("sys_menu m").Distinct(selectFields)
	db.Joins("left join sys_role_menu rm on m.id = rm.menu_id")
	db.Joins("left join sys_user_role ur on rm.role_id = ur.role_id")
	db.Joins("left join sys_role ro on ur.role_id = ro.id")
	db.Where("ur.user_id = ?", userId)
	if listReq.MenuName != "" {
		db.Where("m.menu_name like ?", "%"+listReq.MenuName+"%")
	}
	if listReq.Visible != "" {
		db.Where("m.visible = ?", listReq.Visible)
	}
	if listReq.Status != "" {
		db.Where("m.status = ?", listReq.Status)
	}
	result := make([]*system.SysMenu, 0)
	err := db.Order("parent_id, order_num").Find(&result).Error
	return result, err
}

/*
 */
func (s *SysMenuService) GetMenuListByRoleId(roleId uint64) ([]string, error) {
	sysRole := &system.SysRole{}
	if roleItemResp, err := sysRole.GetById(nil, roleId); err != nil {
		return nil, err
	} else if roleItemResp == nil {
		return nil, nil
	} else {
		if menuIds, err := new(system.SysMenu).GetMenuListByRoleId(roleId, roleItemResp.MenuCheckStrictly); err != nil {
			return nil, err
		} else {
			return menuIds, nil
		}
	}
}

/*
select distinct m.id, m.parent_id, m.menu_name, m.path, m.component, m.`query`,
m.route_name, m.visible, m.status, ifnull(m.perms,”) as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.created_at

	from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0
	order by m.parent_id, m.order_num;
*/
func (s *SysMenuService) GetAllMenuForTree() ([]*system.SysMenu, error) {
	selectFields := []string{
		" m.id",
		"m.parent_id",
		"m.menu_name",
		"m.path",
		"m.component",
		"m.query",
		"m.route_name",
		"m.visible",
		"m.status",
		"ifnull(m.perms,'') as perms",
		"m.is_frame",
		"m.is_cache",
		"m.menu_type",
		"m.icon",
		"m.order_num",
		"m.created_at",
	}
	var menus []*system.SysMenu
	if err := base.BaseDb.Table("sys_menu m").Distinct(strings.Join(selectFields, ",")).
		Where("m.menu_type in (?) and m.status = ?", []string{"M", "C"}, "0").
		Order("m.parent_id, m.order_num").Find(&menus).Error; err != nil {
		return nil, err
	}
	return menus, nil
}

/*
select distinct m.id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.route_name, m.visible, m.status,
ifnull(m.perms,”) as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.created_at

	from sys_menu m
		 left join sys_role_menu rm on m.id = rm.menu_id
		 left join sys_user_role ur on rm.role_id = ur.role_id
		 left join sys_role ro on ur.role_id = ro.id
		 left join sys_user u on ur.user_id = u.id
	where u.id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0  AND ro.status = 0
	order by m.parent_id, m.order_num
*/
func (s *SysMenuService) GetMenuForTreeByUserId(userId uint64) ([]*system.SysMenu, error) {
	var selectFields = []string{
		" m.id",
		"m.parent_id",
		"m.menu_name",
		"m.path",
		"m.component",
		"m.query",
		"m.route_name",
		"m.visible",
		"m.status",
		"ifnull(m.perms,'') as perms",
		"m.is_frame",
		"m.is_cache",
		"m.menu_type",
		"m.icon",
		"m.order_num",
		"m.created_at",
	}
	var menus []*system.SysMenu
	if err := base.BaseDb.Table("sys_menu m").Distinct(strings.Join(selectFields, ",")).
		Joins("left join sys_role_menu rm on m.id = rm.menu_id").
		Joins("left join sys_user_role ur on rm.role_id = ur.role_id").
		Joins("left join sys_role ro on ur.role_id = ro.id").
		Joins("left join sys_user u on ur.user_id = u.id").
		Where("u.id = ? and m.menu_type in (?) and m.status = ? and ro.status = ?", userId, []string{"M", "C"}, "0", "0").
		Order("m.parent_id, m.order_num").Find(&menus).Error; err != nil {
		return nil, err
	}
	return menus, nil
}

func (s *SysMenuService) AddMenuById(addReq *s1.SysMenuAddReq) (bool, error) {
	if unique, err := s.checkMenuNameUnique(&addReq.MenuNameUniqueReq, nil); err != nil {
		return false, err
	} else if !unique {
		return false, moerrors.NewValidateError(consts2.CommCreatFailCode, "菜单名称已存在")
	}
	if addReq.IsFrame == consts.U_YES_FRAME && !utils.IsHttp(addReq.Path) {
		return false, moerrors.NewValidateError(consts2.CommCreatFailCode, "外链必须以http(s)://开头")
	}
	menu := new(system.SysMenu)

	if err := utils.CopyStruct(menu, addReq); err != nil {
		return false, err
	}
	if err := s.GetDb(nil).Create(menu).Error; err != nil {
		return false, err
	}
	return true, nil
}

func (s *SysMenuService) GetMenuById(id uint64) (*system.SysMenu, error) {
	var menu system.SysMenu
	if err := s.GetDb(nil).Where("id = ?", id).First(&menu).Error; err != nil {
		return nil, err
	}
	return &menu, nil
}

func (s *SysMenuService) UpdateMenuById(updateReq *s1.SysMenuUpdateReq) (bool, error) {
	if unique, err := s.checkMenuNameUnique(&updateReq.MenuNameUniqueReq, &request.IDRequest{ID: updateReq.ID}); err != nil {
		return false, err
	} else if !unique {
		return false, moerrors.NewValidateError(consts2.CommUpdateFailCode, "菜单名称已存在")
	}
	if updateReq.IsFrame == consts.U_YES_FRAME && !utils.IsHttp(updateReq.Path) {
		return false, moerrors.NewValidateError(consts2.CommUpdateFailCode, "外链必须以http(s)://开头")
	}
	if updateReq.ID == updateReq.ParentId {
		return false, moerrors.NewValidateError(consts2.CommUpdateFailCode, "上级菜单不能选择自己")
	}
	menu := new(system.SysMenu)

	if err := utils.CopyStruct(menu, updateReq); err != nil {
		return false, err
	}
	if descendant, err := s.checkIsDescendantNode(menu.ID, menu.ParentId); err != nil {
		return false, err
	} else if descendant {
		return false, moerrors.NewValidateError(consts2.CommUpdateFailCode, "上级菜单不能选择自下而上的节点")
	}
	oldMenu := new(system.SysMenu)
	oldMenu.ID = menu.ID
	if err := base.BaseDb.Model(oldMenu).Select("created_by, created_at").First(oldMenu).Error; err != nil {
		return false, err
	}
	menu.CreatedBy = oldMenu.CreatedBy
	menu.CreatedAt = oldMenu.CreatedAt
	var err error
	err = base.BaseDb.Save(menu).Error
	return err == nil, err

}

func (s *SysMenuService) DeleteById(id uint64) (bool, error) {
	sysMenu := new(system.SysMenu)
	sysMenu.ID = id
	if hasChildren, err := sysMenu.HasChildrenMenu(); err != nil {
		return false, err
	} else if hasChildren {
		return false, moerrors.NewValidateError(consts2.CommDeleteFailCode, "存在子菜单，不允许删除")
	}
	sysRoleMenu := system.SysRoleMenu{}
	if exists, err := sysRoleMenu.CheckMenuExistRole(id); err != nil {
		return false, err
	} else if exists {
		return false, moerrors.NewValidateError(consts2.CommDeleteFailCode, "菜单已分配，不允许删除")
	}
	if err := s.GetDb(nil).Where("id = ?", id).Delete(&system.SysMenu{}).Error; err != nil {
		return false, err
	}
	return true, nil
}

func (s *SysMenuService) BuildMenuTreeSelect(menuList []*system.SysMenu) ([]*pogos.TreeSelect, error) {
	if menuTreeResp, err := s.buildMenuTree(menuList); err != nil {
		return nil, err
	} else {
		result := make([]*pogos.TreeSelect, 0)
		for _, menu := range menuTreeResp {
			result = append(result, pogos.NewTreeSelectByMenu(menu))
		}
		return result, nil
	}
}

/*
mysql8
是否为子孙节点

			WITH RECURSIVE cte AS (
	    -- 初始子查询：找到初始的 id
	    SELECT *
	    FROM `sys_menu`
	    WHERE `menu_id` = id

	    UNION ALL

	    -- 递归子查询：找到所有祖先菜单
	    SELECT sm.*
	    FROM `sys_menu` sm
	    JOIN cte ON sm.menu_id = cte.parent_id  limit 2

)
SELECT DISTINCT  *
FROM cte
WHERE `menu_id` = parent_id;

checkedId: parent_id
*/
func (s *SysMenuService) checkIsDescendantNode(id uint64, checkedId uint64) (bool, error) {
	if checkedId == 0 {
		return false, nil
	}
	db := base.BaseDb.Model(&system.SysMenu{})
	sql := "WITH RECURSIVE cte AS (SELECT * FROM `sys_menu` WHERE `id` = ?  UNION ALL SELECT sm.* FROM `sys_menu` sm JOIN cte ON sm.id = cte.parent_id  limit 2) SELECT DISTINCT  * FROM cte WHERE `id` = ?"
	menu := new(system.SysMenu)
	err := db.Raw(sql, checkedId, id).Find(menu).Error
	return err == nil && menu.ID > 0, err
}

func (s *SysMenuService) genMenuTreeByList(sysMenus []*system.SysMenu, rootId uint64) ([]*system2.SysMenuTreeResp, error) {
	var menuTree []*system2.SysMenuTreeResp
	var err error
	lo.ForEach(sysMenus, func(menu *system.SysMenu, index int) {
		if menu.ParentId == rootId {
			treeItem := new(system2.SysMenuTreeResp)
			if err = utils.CopyStruct(treeItem, menu); err != nil {
				framework.MoLogger.Debug("copy menu fail")
				return
			}
			if err = s.genMenuTreeRecursive(sysMenus, treeItem); err != nil {
				framework.MoLogger.Debug("copy menu Recursive fail")
				return
			}
			menuTree = append(menuTree, treeItem)
		}
	})
	return menuTree, err
}

func (s *SysMenuService) buildMenuTree(sysMenus []*system.SysMenu) ([]*system2.SysMenuTreeResp, error) {
	result := make([]*system2.SysMenuTreeResp, 0)
	menuIds := lo.Map(sysMenus, func(menu *system.SysMenu, index int) uint64 {
		return menu.ID
	})
	for _, menu := range sysMenus {
		if !lo.Contains(menuIds, menu.ParentId) {
			treeItem := new(system2.SysMenuTreeResp)

			if err := utils.CopyStruct(treeItem, menu); err != nil {
				return nil, err
			}
			if err := s.genMenuTreeRecursive(sysMenus, treeItem); err != nil {
				return nil, err
			}
			result = append(result, treeItem)
		}
	}
	return result, nil
}

/*
select menu_id, menu_name, parent_id, order_num, path, component, `query`, route_name, is_frame, is_cache, menu_type, visible, status, ifnull(perms,”) as perms, icon, create_time

	from sys_menu

where menu_name=#{menuName} and parent_id = #{parentId} limit 1
*/
func (s *SysMenuService) checkMenuNameUnique(uniqueReq *s1.MenuNameUniqueReq, idReq *request.IDRequest) (bool, error) {
	menu := system.SysMenu{}
	if err := base.BaseDb.Where("menu_name = ? and parent_id = ?", uniqueReq.MenuName, uniqueReq.ParentId).First(&menu).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return true, nil
		}
		return false, err
	}
	return idReq == nil || menu.MoModel.ID == idReq.ID, nil
}

func (s *SysMenuService) genMenuTreeRecursive(sysMenus []*system.SysMenu, parentNode *system2.SysMenuTreeResp) (err error) {
	if parentNode.MoModel.ID == 0 {
		framework.MoLogger.Debug("arithmetic error")
		return nil
	}
	lo.ForEach(sysMenus, func(menu *system.SysMenu, index int) {
		if menu.ParentId == parentNode.MoModel.ID {
			treeItem := new(system2.SysMenuTreeResp)
			if err = utils.CopyStruct(treeItem, menu); err != nil {
				framework.MoLogger.Debug("copy menu fail")
				return
			}
			if err = s.genMenuTreeRecursive(sysMenus, treeItem); err != nil {
				framework.MoLogger.Debug("copy menu Recursive fail")
				return
			}
			parentNode.Children = append(parentNode.Children, treeItem)
		}
	})
	return nil
}

// GetRouteName generates a route name based on the menu.
func getRouteName(menu *system.SysMenu) string {
	// 非外链并且是一级目录（类型为目录）
	if isMenuFrame(menu) {
		return ""
	}
	return getRouteNameOrPath(menu.RouteName, menu.Path)

}

// GetRouteName generates a route name based on the provided route name and path.
func getRouteNameOrPath(name, path string) string {
	routerName := name
	if name == "" {
		routerName = path
	}
	if routerName == "" {
		return ""
	}
	return strings.ToUpper(routerName[:1]) + routerName[1:]
}

// GetRouterPath generates a router path based on the menu.
func getRouterPath(menu *system.SysMenu) string {
	routerPath := menu.Path
	if menu.ParentId != 0 && isInnerLink(menu) {
		routerPath = innerLinkReplaceEach(routerPath)
	}
	if menu.ParentId == 0 && consts.U_TYPE_DIR == menu.MenuType && consts.U_NO_FRAME == menu.IsFrame {
		routerPath = "/" + menu.Path
	}
	return routerPath
}

// GetComponent generates a component path based on the menu.
func getComponent(menu *system.SysMenu) string {
	component := consts.U_LAYOUT
	if menu.Component != "" && !isMenuFrame(menu) {
		component = menu.Component
	} else if menu.Component == "" && menu.ParentId != 0 && isInnerLink(menu) {
		component = consts.U_INNER_LINK
	} else if menu.Component == "" && isParentView(menu) {
		component = consts.U_PARENT_VIEW
	}
	return component
}

/**
 * 是否为parent_view组件
 *
 * @param menu 菜单信息
 * @return 结果
 */
func isParentView(menu *system.SysMenu) bool {
	return menu.ParentId != 0 && consts.U_TYPE_DIR == menu.MenuType
}

// IsMenuFrame checks if the menu is a frame.
func isMenuFrame(menu *system.SysMenu) bool {
	return menu.ParentId == 0 && consts.U_TYPE_MENU == menu.MenuType && menu.IsFrame == consts.U_NO_FRAME
}

// IsInnerLink checks if the menu is an inner link.
func isInnerLink(menu *system.SysMenu) bool {
	return menu.IsFrame == consts.U_NO_FRAME && utils.IsHttp(menu.Path)
}

// InnerLinkReplaceEach replaces each part of the inner link.
func innerLinkReplaceEach(path string) string {
	path = strings.ReplaceAll(path, consts2.HTTP, "")
	path = strings.ReplaceAll(path, consts2.HTTPS, "")
	path = strings.ReplaceAll(path, consts2.WWW, "")
	path = strings.ReplaceAll(path, ".", "/")
	path = strings.ReplaceAll(path, ":", "/")
	return path
}
